using System;
using System.Diagnostics;

namespace Dida
{
	/// <summary>
	/// A Viewpoint describes a point of view at a 3D scene. It contains 
	/// information about the position and orientation of the point of view, 
	/// aswell as projection parameters like <see cref = "FOV">FOV</see>,
	/// <see cref = "NearPlane">NearPlane</see> and <see cref = "FarPlane">
	/// FarPlane</see>.
	/// </summary>
	/// <remarks>
	/// <para>
	/// This class can be seen as a container for all information that's
	/// needed to transform and project a 3D scene to the 2D projection,
	/// regarding to the current point of view.
	/// </para>
	/// <para>
	/// Because the aspect ratio is more related to the render target than
	/// to the viewpoint, and because it can happen that you would like
	/// to use the same viewpoint to render to multiple render targets,
	/// the aspect ratio isn't part of the viewpoint, and yet it's passed 
	/// as a parameter for the <see cref = "CalcMatrices">CalcMatrices</see> method.
	/// </para>
	/// </remarks>
	public class Viewpoint
	{
		float fov;
		float nearPlane;
		float farPlane;
		
		Vector3D pos;
		Quaternion rot;

		Matrix4 projMat;
		Matrix4 viewMat;
		Matrix4 fullMat;

		public Viewpoint(float fov, float nearPlane, float farPlane)
		{
			this.fov = fov;
			this.nearPlane = nearPlane;
			this.farPlane = farPlane;

			rot = Quaternion.Identity;
		}

		public Viewpoint(Vector3D pos,EulerRot rot,float fov,
			float nearPlane, float farPlane)
			: this(fov,nearPlane,farPlane)
		{
			this.pos = pos;
			this.rot = rot.ToQuaternion();
		}

		public void CalcMatrices(float aspect)
		{
			projMat = Matrix4.GetProjectionMatrix(fov, aspect, nearPlane, farPlane);
			if(viewMat == null)
				viewMat = Matrix4.GetTranslationMatrix(-pos) * rot.GetInverseMatrix();
			fullMat = viewMat * projMat;
		}

		/// <summary>
		/// Gets or sets the position of this View.
		/// </summary>
		/// <remarks>
		/// Setting this property invalidates the <see cref = "ViewMatrix">ViewMatrix</see>.
		/// You will have to call <see cref = "CalcMatrices">CalcMatrices</see> before
		/// you can access the <see cref = "ViewMatrix">ViewMatrix</see> property again.
		/// </remarks>
		public Vector3D Position
		{
			get
			{
				return pos;
			}

			set
			{
				pos = value;

				// the view matrix isn't valid any more
				viewMat = null;
			}
		}

		/// <summary>
		/// Sets the rotation of this view, as an <see cref = "EulerRot">EulerRot</see>.
		/// </summary>
		/// <remarks>
		/// Calling this method invalidates the <see cref = "ViewMatrix">ViewMatrix</see>.
		/// You will have to call <see cref = "CalcMatrices">CalcMatrices</see> before
		/// you can access the <see cref = "ViewMatrix">ViewMatrix</see> property again.
		/// </remarks>
		/// <param name="rot">The new rotation.</param>
		public void SetRotation(EulerRot rot)
		{
			this.rot = rot.ToQuaternion();
			viewMat = null;
		}

		/// <summary>
		/// Gets or sets the field of view for this <see cref = "Viewpoint">Viewpoint</see>.
		/// </summary>
		/// <remarks>
		/// Setting this property invalidates the projection matrix, so before
		/// you can use the projection matrix again, you have to call the
		/// <see cref = "CalcMatrices">CalcMatrices</see> method.
		/// </remarks>
		public float FOV
		{
			get { return fov; }
			set
			{
				fov = value;
				// the projection matrix isn't valid any more
				projMat = null;
			}
		}

		/// <summary>
		/// <para>
		/// Gets or sets the near clipping plane of this <see cref = "Viewpoint">Viewpoint</see>.
		/// </para>
		/// <para>
		/// Setting this property invalidates the projection matrix, so before
		/// you can use the projection matrix again, you have to call the
		/// <see cref = "CalcMatrices">CalcMatrices</see> function.
		/// </para>
		/// </summary>
		public float NearPlane
		{
			get { return nearPlane; }
			set
			{
				nearPlane = value;
				// the projection matrix isn't valid nay more
				projMat = null;
			}
		}

		/// <summary>
		/// <para>
		/// Gets or sets the far clipping plane of this <see cref = "Viewpoint">Viewpoint</see>.
		/// </para>
		/// <para>
		/// Setting this property invalidates the projection matrix, so before
		/// you can use the projection matrix again, you have to call the
		/// <see cref = "CalcMatrices">CalcMatrices</see> function.
		/// </para>
		/// </summary>
		public float FarPlane
		{
			get { return farPlane; }
			set
			{
				farPlane = value;
				// the projection matrix isn't valid any more.
				projMat = null;
			}
		}

		public Matrix4 ViewMatrix
		{
			get
			{
				Debug.Assert(viewMat != null, "You have to call the " + 
					"CalcMatrices method before you can use the " + 
					"matrices of this View");
				return viewMat;
			}
		}

		public Matrix4 ProjectionMatrix
		{
			get
			{
				Debug.Assert(projMat != null, "You have to call the " +
					"CalcMatrices method before you can use the " +
					"matrices of this View");
				return projMat; 
			}
		}

		public Matrix4 FullMatrix
		{
			get
			{
				Debug.Assert(fullMat != null, "You have to call the " +
					"CalcMatrices method before you can use the " +
					"matrices of this View");
				return fullMat;
			}
		}

		/// <summary>
		/// <para>
		/// Returns a 3D vector, that describes the ray that goes from
		/// this viewpoint's position, though the given 2D point on
		/// the viewpoint's screen, into the scene.
		/// </para>
		/// <para>
		/// This is the ray that could be used in a raytracer, as the ray 
		/// for the pixel at the given position.
		/// </para>
		/// <para>
		/// This method requires the matrices of this Viewport to be calculated, 
		/// so in order to use it, you must have called the
		/// <see cref = "CalcMatrices">CalcMatrices</see> method before.
		/// </para>
		/// </summary>
		/// <param name="pt">The position of the point on the screen where the
		/// ray goes through. The values for this coordinate range from -1,-1 for
		/// the bottom left corner, to 1,1 for the top right corner of the screen.
		/// </param>
		/// <param name="aspect">The aspect ratio.</param>
		/// <returns>A vector representing the ray of the given projected point.
		/// This vector will always have length 1</returns>
		public Vector3D GetRay(Vector2D pt,float aspect)
		{
			Debug.Assert(projMat != null, "You have to call the " +
				"CalcMatrices method in order to use this function.");

			float halfFov = fov * .5f;
			float yScale = (float)Math.Tan(halfFov);
			float xScale = yScale * aspect;

			Vector3D ret = new Vector3D(0, 0, 0);
			ret += viewMat.GetBaseVector(0) * (pt.x * xScale);
			ret += viewMat.GetBaseVector(1) * (pt.y * yScale);
			ret += viewMat.GetBaseVector(2);
			ret.Normalize();
			return ret;
		}

		public EulerRot EulerRot
		{
			get
			{
				return rot.ToEulerRot();
			}

			set
			{
				rot = new Quaternion(value);
				viewMat = null;
			}
		}

		public static Viewpoint CreateOverview(Vector3D target, float yaw, float pitch,
			float dist, float nearPlane,float farPlane,float fov)
		{
			Viewpoint ret = new Viewpoint(fov, nearPlane, farPlane);
			
			Vector3D pos = new Vector3D(0, 0, -dist);
			pos.RotatePitch(pitch);
			pos.RotateYaw(yaw);			
			pos += target;

			ret.Position = pos;
			ret.EulerRot = new EulerRot(yaw, pitch, 0);

			return ret;
		}

		public static Viewpoint CreateOverview(Vector3D target, float yaw, float pitch,
			float dist, float fov)
		{
			return CreateOverview(target, yaw, pitch, dist, .1f, 1000.0f, fov);
		}
	}
}